/*
* BearLibTerminal
* Copyright (C) 2013 Cfyz
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#include "Bitmap.hpp"
#include <istream>
#include <cstdint>
#include <functional>
#include <stdexcept>
#include <cmath>

namespace BearLibTerminal
{
    Bitmap LoadBMP( std::istream &stream )
    {
        struct BitmapFileHeader
        {
            uint16_t header;                    // 0x42 0x4D in hexadecimal, same as BM in ASCII
            uint16_t size_low, size_high;        // The size of the BMP file in bytes
            uint16_t reserved1, reserved2;        // Reserved
            uint16_t offset_low, offset_high;    // The offset of the byte where the bitmap image data can be found.
        };

        struct BitmapInfoHeader
        {
            uint32_t header_size;                // The size of this header (40 bytes)
            int32_t width;                        // The bitmap width (signed integer).
            int32_t height;                        // The bitmap height (signed integer).
            uint16_t planes;                    // The number of color planes. Must be set to 1.
            uint16_t bpp;                        // The number of bits per pixel. Typical values are 1, 4, 8, 16, 24 and 32.
            uint32_t compression;                // The compression method.
            uint32_t image_size;                // The size of the raw bitmap data.
            int32_t xppm;                        // The horizontal resolution of the image (pixel per meter, signed integer).
            int32_t yppm;                        // The vertical resolution of the image.
            uint32_t colors_used;                // The number of colors in the color palette, or 0 to default to 2n.
            uint32_t colors_important;            // The number of important colors used; generally ignored.
        };

        BitmapFileHeader file_header;
        BitmapInfoHeader info_header;
        std::vector <Color> palette;

        stream.read( reinterpret_cast<char *>(&file_header), sizeof( file_header ));
        stream.read( reinterpret_cast<char *>(&info_header), sizeof( info_header ));

        if ( !stream.good( ))
        {
            throw std::runtime_error( "invalid bitmap image" );
        }

        if ( info_header.header_size != 40 )
        {
            throw std::runtime_error( "unsupported DIB image format" );
        }

        if ( info_header.bpp != 8 && info_header.bpp != 24 && info_header.bpp != 32 )
        {
            throw std::runtime_error( "unsupported color depth" );
        }

        if ( info_header.compression != 0 )
        {
            throw std::runtime_error( "unsupported compression level" );
        }

        if ( info_header.bpp < 24 )
        {
            size_t palette_entries = info_header.colors_used;
            if ( palette_entries == 0 )
            { palette_entries = ( 1 << info_header.bpp ); }
            palette.reserve( palette_entries );

            for ( size_t i = 0; i < palette_entries; i++ )
            {
                Color entry;
                stream.read( reinterpret_cast<char *>(&entry), sizeof( entry ));
                entry.a = 0xFF;
                palette.push_back( entry );
            }
        }

        size_t offset = ( file_header.offset_high << 16 ) + file_header.offset_low;
        std::streamoff position = stream.tellg( );
        stream.ignore( offset - position );

        if ( !stream.good( ))
        {
            throw std::runtime_error( "invalid bitmap image" );
        }

        Bitmap result( Size( info_header.width, ( int ) std::abs( info_header.height )), Color( ));

        std::function <void( Point )> pixel_readers[] =
                {
                        [ & ]( Point pt ) -> void
                        {
                            // 32-bit BGRA, full Color struct
                            Color value;
                            stream.read( reinterpret_cast<char *>(&value), 4 );
                            result( pt ) = value;
                        },
                        [ & ]( Point pt ) -> void
                        {
                            // 24-bit BGR, 3/4 Color struct
                            Color value;
                            stream.read( reinterpret_cast<char *>(&value), 3 );
                            value.a = 0xFF;
                            result( pt ) = value;
                        },
                        [ & ]( Point pt ) -> void
                        {
                            // Palletized I, lookup
                            uint8_t index;
                            stream.read( reinterpret_cast<char *>(&index), 1 );
                            result( pt ) = palette[ index ];
                        }
                };

        std::function <void( Point )> pixel_reader;
        switch ( info_header.bpp )
        {
            case 32:
                pixel_reader = pixel_readers[ 0 ];
                break;

            case 24:
                pixel_reader = pixel_readers[ 1 ];
                break;

            case 8:
                pixel_reader = pixel_readers[ 2 ];
                break;
        }

        size_t line_size = ( info_header.bpp / 8 ) * info_header.width;
        int padding = 4;
        int modulo = line_size % padding;
        size_t line_stride = line_size + ( modulo > 0 ? padding - modulo : 0 );
        size_t line_padding = line_stride - line_size;

        auto line_reader = [ & ]( int y ) -> void
        {
            for ( int x = 0; x < ( int ) info_header.width; x++ )
            {
                pixel_reader( Point( x, y ));
            }
            stream.ignore( line_padding );
        };

        if ( info_header.height > 0 )
        {
            // For positive height values rows are stored from bottom to top
            for ( int y = info_header.height - 1; y >= 0; y-- )
            { line_reader( y ); }
        }
        else
        {
            // For negative height rows are top to bottom
            for ( int y = 0; y < -info_header.height; y++ )
            { line_reader( y ); }
        }

        return result;
    }
}
